// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "cc/input/scrollbar_animation_controller_thinning.h"

#include "base/memory/ptr_util.h"
#include "base/time/time.h"
#include "cc/layers/layer_impl.h"
#include "cc/layers/scrollbar_layer_impl_base.h"
#include "cc/trees/layer_tree_impl.h"

namespace {
const float kIdleThicknessScale = 0.4f;
const float kDefaultMouseMoveDistanceToTriggerAnimation = 25.f;
}

namespace cc {

std::unique_ptr<ScrollbarAnimationControllerThinning>
ScrollbarAnimationControllerThinning::Create(
    int scroll_layer_id,
    ScrollbarAnimationControllerClient* client,
    base::TimeDelta delay_before_starting,
    base::TimeDelta resize_delay_before_starting,
    base::TimeDelta fade_duration,
    base::TimeDelta thinning_duration)
{
    return base::WrapUnique(new ScrollbarAnimationControllerThinning(
        scroll_layer_id, client, delay_before_starting,
        resize_delay_before_starting, fade_duration, thinning_duration));
}

ScrollbarAnimationControllerThinning::ScrollbarAnimationControllerThinning(
    int scroll_layer_id,
    ScrollbarAnimationControllerClient* client,
    base::TimeDelta delay_before_starting,
    base::TimeDelta resize_delay_before_starting,
    base::TimeDelta fade_duration,
    base::TimeDelta thinning_duration)
    : ScrollbarAnimationController(scroll_layer_id,
        client,
        delay_before_starting,
        resize_delay_before_starting)
    , opacity_(0.0f)
    , captured_(false)
    , mouse_is_over_scrollbar_(false)
    , mouse_is_near_scrollbar_(false)
    , thickness_change_(NONE)
    , mouse_move_distance_to_trigger_animation_(
          kDefaultMouseMoveDistanceToTriggerAnimation)
    , fade_duration_(fade_duration)
    , thinning_duration_(thinning_duration)
    , current_animating_property_(OPACITY)
{
    ApplyOpacity(0.f);
    ApplyThumbThicknessScale(kIdleThicknessScale);
}

ScrollbarAnimationControllerThinning::~ScrollbarAnimationControllerThinning() { }

void ScrollbarAnimationControllerThinning::RunAnimationFrame(float progress)
{
    if (captured_)
        return;

    if (current_animating_property_ == OPACITY)
        ApplyOpacity(1.f - progress);
    else
        ApplyThumbThicknessScale(ThumbThicknessScaleAt(progress));

    client_->SetNeedsRedrawForScrollbarAnimation();
    if (progress == 1.f) {
        StopAnimation();
        if (current_animating_property_ == THICKNESS) {
            thickness_change_ = NONE;
            SetCurrentAnimatingProperty(OPACITY);
            if (!mouse_is_near_scrollbar_)
                PostDelayedAnimationTask(false);
        }
    }
}

const base::TimeDelta& ScrollbarAnimationControllerThinning::Duration()
{
    if (current_animating_property_ == OPACITY)
        return fade_duration_;
    else
        return thinning_duration_;
}

void ScrollbarAnimationControllerThinning::DidMouseDown()
{
    if (!mouse_is_over_scrollbar_ || opacity_ == 0.0f)
        return;

    StopAnimation();
    captured_ = true;
    ApplyOpacity(1.f);
    ApplyThumbThicknessScale(1.f);
}

void ScrollbarAnimationControllerThinning::DidMouseUp()
{
    if (!captured_ || opacity_ == 0.0f)
        return;

    captured_ = false;
    StopAnimation();

    if (!mouse_is_near_scrollbar_) {
        SetCurrentAnimatingProperty(THICKNESS);
        thickness_change_ = DECREASE;
        StartAnimation();
    } else {
        SetCurrentAnimatingProperty(OPACITY);
    }
}

void ScrollbarAnimationControllerThinning::DidMouseLeave()
{
    if (!mouse_is_over_scrollbar_ && !mouse_is_near_scrollbar_)
        return;

    mouse_is_over_scrollbar_ = false;
    mouse_is_near_scrollbar_ = false;

    if (captured_ || opacity_ == 0.0f)
        return;

    thickness_change_ = DECREASE;
    SetCurrentAnimatingProperty(THICKNESS);
    StartAnimation();
}

void ScrollbarAnimationControllerThinning::DidScrollUpdate(bool on_resize)
{
    if (captured_)
        return;

    ScrollbarAnimationController::DidScrollUpdate(on_resize);
    ApplyOpacity(1.f);
    ApplyThumbThicknessScale(mouse_is_near_scrollbar_ ? 1.f
                                                      : kIdleThicknessScale);
    SetCurrentAnimatingProperty(OPACITY);

    // Don't fade out the scrollbar when mouse is near.
    if (mouse_is_near_scrollbar_)
        StopAnimation();
}

void ScrollbarAnimationControllerThinning::DidScrollEnd()
{
    ScrollbarAnimationController::DidScrollEnd();

    // Don't fade out the scrollbar when mouse is near.
    if (mouse_is_near_scrollbar_)
        StopAnimation();
}

void ScrollbarAnimationControllerThinning::DidMouseMoveNear(float distance)
{
    bool mouse_is_over_scrollbar = distance == 0.0f;
    bool mouse_is_near_scrollbar = distance < mouse_move_distance_to_trigger_animation_;

    if (captured_ || opacity_ == 0.0f) {
        mouse_is_near_scrollbar_ = mouse_is_near_scrollbar;
        mouse_is_over_scrollbar_ = mouse_is_over_scrollbar;
        return;
    }

    if (mouse_is_over_scrollbar == mouse_is_over_scrollbar_ && mouse_is_near_scrollbar == mouse_is_near_scrollbar_)
        return;

    if (mouse_is_over_scrollbar_ != mouse_is_over_scrollbar)
        mouse_is_over_scrollbar_ = mouse_is_over_scrollbar;

    if (mouse_is_near_scrollbar_ != mouse_is_near_scrollbar) {
        mouse_is_near_scrollbar_ = mouse_is_near_scrollbar;
        thickness_change_ = mouse_is_near_scrollbar_ ? INCREASE : DECREASE;
    }

    SetCurrentAnimatingProperty(THICKNESS);
    StartAnimation();
}

bool ScrollbarAnimationControllerThinning::ScrollbarsHidden() const
{
    return opacity_ == 0.0f;
}

float ScrollbarAnimationControllerThinning::ThumbThicknessScaleAt(
    float progress)
{
    if (thickness_change_ == NONE)
        return mouse_is_near_scrollbar_ ? 1.f : kIdleThicknessScale;
    float factor = thickness_change_ == INCREASE ? progress : (1.f - progress);
    return ((1.f - kIdleThicknessScale) * factor) + kIdleThicknessScale;
}

float ScrollbarAnimationControllerThinning::AdjustScale(
    float new_value,
    float current_value,
    AnimationChange animation_change,
    float min_value,
    float max_value)
{
    float result;
    if (animation_change == INCREASE && current_value > new_value)
        result = current_value;
    else if (animation_change == DECREASE && current_value < new_value)
        result = current_value;
    else
        result = new_value;
    if (result > max_value)
        return max_value;
    if (result < min_value)
        return min_value;
    return result;
}

void ScrollbarAnimationControllerThinning::ApplyOpacity(float opacity)
{
    for (ScrollbarLayerImplBase* scrollbar : Scrollbars()) {
        if (!scrollbar->is_overlay_scrollbar())
            continue;
        float effective_opacity = scrollbar->CanScrollOrientation() ? opacity : 0;
        PropertyTrees* property_trees = scrollbar->layer_tree_impl()->property_trees();
        // If this method is called during LayerImpl::PushPropertiesTo, we may not
        // yet have valid layer_id_to_effect_node_index entries as property trees
        // are pushed after layers during activation. We can skip updating opacity
        // in that case as we are only registering a scrollbar and because opacity
        // will be overwritten anyway when property trees are pushed.
        if (property_trees->IsInIdToIndexMap(PropertyTrees::TreeType::EFFECT,
                scrollbar->id())) {
            property_trees->effect_tree.OnOpacityAnimated(
                effective_opacity,
                property_trees->layer_id_to_effect_node_index[scrollbar->id()],
                scrollbar->layer_tree_impl());
        }
    }

    bool previouslyVisible = opacity_ > 0.0f;
    bool currentlyVisible = opacity > 0.0f;

    opacity_ = opacity;

    if (previouslyVisible != currentlyVisible)
        client_->DidChangeScrollbarVisibility();
}

void ScrollbarAnimationControllerThinning::ApplyThumbThicknessScale(
    float thumb_thickness_scale)
{
    for (ScrollbarLayerImplBase* scrollbar : Scrollbars()) {
        if (!scrollbar->is_overlay_scrollbar())
            continue;

        scrollbar->SetThumbThicknessScaleFactor(AdjustScale(
            thumb_thickness_scale, scrollbar->thumb_thickness_scale_factor(),
            thickness_change_, kIdleThicknessScale, 1));
    }
}

void ScrollbarAnimationControllerThinning::SetCurrentAnimatingProperty(
    AnimatingProperty property)
{
    if (current_animating_property_ == property)
        return;

    StopAnimation();
    current_animating_property_ = property;
    if (current_animating_property_ == THICKNESS)
        ApplyOpacity(1.f);
}

} // namespace cc
